import chalk, { Color } from 'chalk'
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
import fs from 'fs-extra'
import { debounce, once } from 'lodash-es'
import path from 'path'
import * as rollup from 'rollup'
import { URL } from 'url'
import { Plugin } from 'vite'
import { buildElectron } from './build'
import { fixViteConfig, generateElectronRollupOptions, getDirConfig, UserOptions } from './options'
import { isElectronModule, log } from './utils'
export { default as assetsPlugin } from '@xyh19/rollup-plugin-assets'
export { default as definePlugin } from '@xyh19/rollup-plugin-define'
export { default as externalsPlugin } from '@xyh19/rollup-plugin-node-externals'
export { UserOptions }
function removeJunk(chunk: string) {
  // Example: 2018-08-10 22:48:42.866 Electron[90311:4883863] *** WARNING: Textured window <AtomNSWindow: 0x7fb75f68a770>
  if (/\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test(chunk)) {
    return false
  }
  // Example: [90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught (in promise) Error: Could not instantiate: ProductRegistryImpl.Registry", source: chrome-devtools://devtools/bundled/inspector.js (105)
  if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(chunk)) {
    return false
  }
  // Example: ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
  if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(chunk)) {
    return false
  }
  return chunk
}

const _console: any = { ...console }
const createLog = once(() => {
  const _createLog = (method: string, id: string, color: typeof Color & keyof typeof chalk) => {
    const _id = chalk.bold[color](`[${id}]`)
    return (...args: any[]) => {
      if (typeof args[0] === 'string') {
        args[0] = _id + args[0]
      } else {
        args.unshift(_id)
      }
      return _console[method].call(console, ...args)
    }
  }

  console.log = _createLog('log', 'RENDER', 'green')
  console.info = _createLog('info', 'RENDER', 'green')
  console.debug = _createLog('debug', 'RENDER', 'green')
  console.error = _createLog('error', 'RENDER', 'red')
})

export default function electronBuilder(options: UserOptions = {}): Plugin {
  createLog()

  let config: Parameters<NonNullable<Plugin['configResolved']>>[0]
  let electronProcess: ChildProcessWithoutNullStreams | undefined
  let manualRestart = false
  let watcher: rollup.RollupWatcher
  let appOptions: rollup.RollupWatchOptions
  let forceRestart: () => boolean | Promise<boolean>
  const { mainDir, root, appEntry, resolveEntryName } = getDirConfig(options)
  const BUILTIN_PREFIX = 'NODE_BUILTIN:'
  const BUILTIN_SUFFIX = '.cjs'

  function startMain() {
    return new Promise(async resolve => {
      const opts = appOptions

      const _restartElectron = debounce(async event => {
        if (await forceRestart()) {
          if (electronProcess) {
            electronProcess.kill()
            electronProcess = undefined
            manualRestart = true
          }
        }

        startElectron()
        setTimeout(() => (manualRestart = false), 2500)
        resolve(event)
      }, 50)

      watcher = rollup.watch(opts)
      watcher.on('change', filename => {
        // 主进程日志部分
        log(`file '${filename}' changed`, 'MAIN', 'green')
      })

      watcher.on('event', async event => {
        if (event.code === 'END') {
          _restartElectron(event)
        } else if (event.code === 'ERROR') {
          log(event, 'MAIN-ERROR', 'red')

          process.exit()
        }
      })
    })
  }

  const startElectron = () => {
    if (electronProcess) return
    electronProcess = spawn(
      require('electron') as any as string,
      [path.join(mainDir, resolveEntryName(appEntry)), '--trace-warnings'],
      {
        env: {
          ...process.env,
          ELECTRON: require('electron/package.json').version,
          NODE_ENV: config.mode,
          DEBUG: process.env.DEBUG || 'true'
        }
      }
    )

    electronProcess.stdout.on('data', data => {
      log(removeJunk(String(data).trimEnd()), 'ELECTRON', 'blue')
    })
    electronProcess.stderr.on('data', data => {
      log(removeJunk(String(data).trimEnd()), 'ELECTRON', 'red')
    })

    electronProcess.on('error', err => {
      log(err, 'ELECTRON-ERROR', 'red')
    })

    electronProcess.on('exit', (code, signal) => {
      log(`code=${code}, signal=${signal}`, 'ELECTRON-EXIT', 'greenBright')
      if (!manualRestart) {
        process.exit()
      }
    })
  }

  return {
    enforce: 'pre',
    name: 'electron',
    config(config, env) {
      fixViteConfig(options, config, env)
    },
    async configResolved(_config) {
      config = _config
      appOptions = generateElectronRollupOptions(options, config)
      const _forceRestart = options.forceRestart
      if (typeof _forceRestart === 'function') {
        forceRestart = _forceRestart
      } else if (typeof _forceRestart === 'boolean') {
        forceRestart = () => _forceRestart
      } else if (_forceRestart! instanceof Promise) {
        forceRestart = async () => !!(await _forceRestart)
      } else {
        forceRestart = () => false
      }
      await fs.remove(mainDir)
    },
    configureServer(server) {
      server.httpServer!.on('listening', async () => {
        let addr = server.httpServer!.address()!
        const { port, address } = addr as import('net').AddressInfo
        const Protocol = config.server?.https ? 'https' : 'http'
        addr = `${Protocol}://${address}:${port}`

        process.env.DEV_PORT = String(port)
        process.env.DEV_URL = new URL(config.base, addr).toString()

        await startMain()
      })
    },
    outputOptions(options) {
      const generatedCode =
        options.generatedCode && typeof options.generatedCode === 'object'
          ? options.generatedCode
          : (options.generatedCode = { preset: options.generatedCode })
      generatedCode.arrowFunctions = generatedCode.arrowFunctions ?? true
      generatedCode.constBindings = generatedCode.constBindings ?? true
      options.sourcemapExcludeSources = options.sourcemapExcludeSources ?? true
      return null
    },
    async buildStart() {
      if (config.command === 'build') {
        try {
          const opts = appOptions
          const builder = await rollup.rollup(opts)
          if (Array.isArray(opts.output)) {
            for (const output of opts.output) {
              await builder.write(output)
            }
          } else if (opts.output) {
            await builder.write(opts.output)
          }
        } catch (error) {
          log(error, 'MAIN-ERROR', 'red')

          process.exit()
        }
      }
    },
    closeBundle() {
      electronProcess?.kill()
      electronProcess = undefined
      if (config.command === 'build') {
        return buildElectron(options, root, mainDir)
      }
    },
    resolveId(source, importer) {
      if (source.startsWith('node:')) {
        source = source.substring('node:'.length)
      }
      if (isElectronModule(source)) {
        return BUILTIN_PREFIX + source + BUILTIN_SUFFIX
      }
    },
    load(id) {
      if (id.startsWith(BUILTIN_PREFIX) && id.endsWith(BUILTIN_SUFFIX)) {
        id = id.slice(0, id.length - BUILTIN_SUFFIX.length).slice(BUILTIN_PREFIX.length)
        return `module.exports = window.require(${JSON.stringify(id)});`
      }
    }
  }
}
